package org.mashart.pink.m.gps.data;

import org.mashart.pink.m.gps.Waypoint;

/**
 * Basic implementation of NMEA sentences parser
 * Supports GPGGA, GPRMC, GPGLL, GPGSA for the moment.
 *
 */
public class NMEAParser extends AbstractParser {

    /**
     * Header of NMEA sentence, including the character '$'
     */
    private static final String GPGGA_HEADER = "$GPGGA";
    private static final String GPRMC_HEADER = "$GPRMC";
    private static final String GPGLL_HEADER = "$GPGLL";
    private static final String GPGSA_HEADER = "$GPGSA";

    /* codes represent the composition of an NMEA sentence.
     * 1 - latitude
     * 2 - longitude
     * 3 - altitude
     * 4 - HDOP
     * 5 - VDOP
     * H - header, e.g, $GPGGA
     * T - utc, e.g, 01:23:45.789
     * N - north/south
     * E - east/west
     * F - the GPS data is fixed(valid) or not
     * V - speed over ground
     * A - course over ground
     * D - date
     * S - number of used satellites
     * C - checksum
     * X - other information, not interesting for us
     */
    private static final String GPGGA_CODE = "HT1N2EFS43XXXXXC";
    private static final String GPRMC_CODE = "HTF1N2EVADXXC";
    private static final String GPGLL_CODE = "H1N2ETFC";
    private static final String GPGSA_CODE = "HFXXXXXXXXXXXXXX45C";

    /**
     * Separator in NMEA sentence
     */
    private static final String NMEA_SEPARATOR = ",";
    private static final String CHECKSUM_SEPARATOR = "*";

    /**
     * Accuracy of the GPS device.
     */
    private int accuracy;

    /**
     * Constructor
     * @param acc accuracy (in meter) of the bluetooth GPS device
     */
    public NMEAParser(int acc){
        accuracy = acc;
    }

    /**
     * If a sentence is valid. For the moment, any sentence with a known
     * header is considered as "valid".
     * @param sentence NMEA sentence
     * @return true if the sentence is valid, false otherwise.
     */
    public boolean isValid(String sentence) {
        return getCode(sentence) != null;
    }

    /**
     * Parse NMEA sentences of multiple formats
     */
    public Waypoint parse(String s){
        Waypoint wp = null;
        // "update" method will create a new waypoint and update it with the NMEA sentence
        return wp = update(wp, s);
    }

    /**
     * Updates a waypoint with the new information in an NMEA sentence.
     * Read the values of properties from the NMEA sentence.
     * If the field is not set yet, set it.
     * If the field is already set, overwrite it.
     */
    public Waypoint update(Waypoint wp, String s){
        //check if the string is a valid sentence: not empty and start with '$'
        if(s == null || s.charAt(0) != '$'){
            return wp;
        }

        //seperate the checksum, if any
//        //currently, we do not check the checksum
//        String checksum;
        String[] ss = split(s, CHECKSUM_SEPARATOR);
        //wrong format: more than one '*' in a sentence
        if(ss.length > 2){
            return wp;
        }
        
        int indexChecksum = s.indexOf( CHECKSUM_SEPARATOR );
        // if there is a '*'
        if ( indexChecksum > 0 ){
            s = s.substring(0, indexChecksum);
//            checksum = s.substring( indexChecksum + 1 );
        }
//        if(ss.length == 2){
//            checksum = ss[1];
//        }else{
//            checksum = null;
//        }

        //break up the sentence into words
//        String[] snt = split(ss[0], NMEA_SEPARATOR);
        String[] snt = split(s, NMEA_SEPARATOR);
        //prepare the code for different sentence format
        String code = getCode(s);
        //incompatible format
        if(code == null){
            return wp;
        }

        wp = update(wp, snt, code);
        return wp;
    }

    /**
     * Read the words one by one, look up to code, and updates the corresponding fields of the Waypoint
     * E.g, the code of a GPRMC sentence is "HTS1N2EVXXXC", we update the timestamp, fix, latitude, n/s, longitude, e/w, speed
     * with the 2, 3, 4, 5, 6, 7, 8th words, and ignore others.
     * If the waypoint in parameter is null, then create one.
     */
    private Waypoint update(Waypoint wp, String[]snt, String code){
        // incompatible sentence: number of the words and length of the code do agree
        if(snt == null || code == null){
            return wp;
        }

        if(wp == null){
            wp = new Waypoint();
        }

        // i goes from 1, header is ignored
        for(int i = 1; i < code.length() && i < snt.length; i++){
            // ignore empty word
            if(snt[i] == null || snt[i].equals("")){
                continue;
            }
            switch(code.charAt(i)){
                case '1':
                    wp.latitude = parseCoordinate(snt[i]);
                    break;
                case 'N':
                    if ( snt[i].equals("S") ){
                        wp.latitude = - wp.latitude;
                    }
//                    wp.ns = snt[i].equals("N") ? true : false;
                    break;
                case '2':
                    wp.longitude = parseCoordinate(snt[i]);
                    break;
                case 'E':
                    if ( snt[i].equals("W") ){
                        wp.longitude = - wp.longitude;
                    }
//                    wp.ew = snt[i].equals("E") ? true : false;
                    break;
                case '3':
                    wp.altitude = Waypoint.round( Float.parseFloat(snt[i]) );
                    break;
                case '4':
                    wp.acch = Float.parseFloat(snt[i]) * accuracy;
                    break;
                case '5':
                    wp.accv = Float.parseFloat(snt[i]) * accuracy;
                    break;
                case 'T':
                    wp.timeMS = parseTimeMS(snt[i]);
                    break;
                case 'F':
                    // the satellite status in one or more sentence is fixed
                    wp.fix |= parseFix(code, snt[i]);
                    break;
                case 'V':
                    wp.speed = Float.parseFloat(snt[i]) * (float)1.852; // 1 knot = 1.852 km/h
                    break;
                case 'A':
                    wp.course = Waypoint.round( Float.parseFloat(snt[i]) );
                    break;
                case 'D':
                    wp.date = snt[i];
                    break;
                case 'S':
                    wp.satellites = (short) Integer.parseInt(snt[i]);
                    break;

                case 'X':
                    //ignore
                    break;
                case 'H':
                    //header, ignore
                    break;
                case 'C':
                    //checksum, ignore
                    break;
                default:
                    //undefined symbol, program should not reach here
                    break;
            }
        }
        return wp;
    }

    /**
     * Parse the ddmm.mmmm format coordinate
     * @param s String in ddmm.mmmm format, e.g. "4430.0000"
     * @return coordinate value in dd.dddd... format, e.g., 44.5
     */
    public double parseCoordinate(String s){
        // parse the ddmm.mmmm format coordinate
        double ddmm = Double.parseDouble(s);
        // calculate the dd.dddddd format value
        int dd = (int) ddmm / 100;
        double mm = ddmm - dd * 100;
        double dd_dd = dd + mm / 60;
        return dd_dd;
    }

    /**
     * Parse the staus
     * For GPGGA, '0' means not fixed, '1','2','3' mean fixed.
     * For GPRMC, 'V' means not fixed, 'A' means fixed.
     */
    private boolean parseFix(String code, String s){
        boolean fixed = false;

        if(code.equals(GPGGA_CODE)){
            if(s.equals("1") || s.equals("2") || s.equals("3")){
                fixed = true;
            }
        }

        else if(code.equals(GPGLL_CODE) || code.equals(GPRMC_CODE)){
            if(s.equals("A")){
                fixed = true;
            }
        }

        else if(code.equals(GPGSA_CODE)){
            if(s.equals("2") || s.equals("3")){
                fixed = true;
            }
        }

        return fixed;
    }

    /**
     * Parse the time, with the format "012345.789", which means 01:23:45.678
     * @return elapsed milliseconds from 00:00:00.000 of today, or -1 if the time format is not compatible
     */
    public int parseTimeMS(String s){
        int timeMS = -1;
        // check if the string is a valid format
        if(s != null && s.length() == 10 && s.charAt(6) == '.'){
            int hh = Integer.parseInt(s.substring(0, 2));
            int mm = Integer.parseInt(s.substring(2, 4));
            int ss = Integer.parseInt(s.substring(4, 6));
            int MMM = Integer.parseInt(s.substring(7, 10));
            // calculate the elapsed milliseconds from 00:00:00.000
            timeMS = hh * 3600000 + mm * 60000 + ss * 1000 + MMM;
        }
        return timeMS;
    }

    /**
     * Get the code of an NMEA sentence
     */
    private String getCode(String stn){
        // check if null
        if(stn == null){
            return null;
        }
        int i = stn.indexOf(NMEA_SEPARATOR);
        if(i < 0){
            return null;
        }
        //read the header
        String hd = stn.substring(0, i);
        //prepare the code for different sentence format
        String code;
        if(hd.equals(GPGGA_HEADER)){
            code = GPGGA_CODE;
        }else if(hd.equals(GPRMC_HEADER)){
            code = GPRMC_CODE;
        }else if(hd.equals(GPGLL_HEADER)){
            code = GPGLL_CODE;
        }else if(hd.equals(GPGSA_HEADER)){
            code = GPGSA_CODE;
        }else{
            //incompatible sentence format
            code = null;
        }
        return code;
    }

    /**
     * Get the timestamp from a NMEA sentence
     * @return elapsed milliseconds from 00:00:00.000 of today,
     * or -1 if the NMEA sentence format does not contain a timestamp or the timestamp string is invalid
     */
    public int getTimeMS(String sentence){
        String timeStr = getWord(sentence, 'T');
        if(timeStr == null){
            return -1;
        }
        // parse the timestamp
        return parseTimeMS(timeStr);
    }

    /**
     * Get the "fixed" attribute from a NMEA sentence
     * @return true if the satellites have fixed the position of mobile device.
     * If the sentence format contains no "fix" status information, return false;
     */
    public boolean getFix(String sentence){
        String fixStr = getWord(sentence, 'F');
        if(fixStr == null){
            return false;
        }
        // parse the  word of "fix"
        return parseFix(getCode(sentence), fixStr);
    }

    /**
     * Get a specified word from a NMEA sentence
     * @return the String value of that word
     */
    public String getWord(String sentence, char token){
        if(sentence == null){
            return null;
        }

        // get the format code of input sentence
        String code = getCode(sentence);
        // incompatible
        if(code == null){
            return null;
        }
        int i = code.indexOf(token);
        // if  'T' does not occur in the code, the sentence format contains no timestamp
        if(i < 0){
            return null;
        }

        // split the sentence into words
        String [] stn = split(sentence, NMEA_SEPARATOR);
        // return the word
        return stn[ i ];
    }
}
